﻿using Letter.IO.Scheduler;
using System;
using System.Buffers;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using Letter.IO.Memorizer;

namespace Letter.Net.Tcp;

sealed class TcpServer : ATcpHost<TcpServerTransportOptions, TcpServerFeatureOptions>, ITcpServer
{
    private static MemoryPool<byte> memoryPool = MemoryPoolProvider.Create<byte>();

    public TcpServer(string name) : base(name)
    {
    }

    private Socket listenSocket;
    private EndPoint localAddress;
    private ChannelSettingsAllotter channelSettingsAllotter;

    private Task acceptTask;

    public override EndPoint BindEndPoint
    {
        get { return localAddress; }
    }

    public override void Build()
    {
        base.Build();
        
        var emptyMaxReadBufferSize = (this.transportOptions.MaxReadBufferSize == null);
        var emptyMaxWriteBufferSize = (this.transportOptions.MaxWriteBufferSize == null);
        var maxReadBufferSize = emptyMaxReadBufferSize ? 0 : this.transportOptions.MaxReadBufferSize.Value;
        var maxWriteBufferSize = emptyMaxWriteBufferSize ? 0 : this.transportOptions.MaxWriteBufferSize.Value;

        var allotter = SchedulerProvider.GetSharedAllotter();
        this.channelSettingsAllotter = new ChannelSettingsAllotter(
                                                                    memoryPool, 
                                                                    maxReadBufferSize,
                                                                    maxWriteBufferSize);
    }

    public override Task BindAsync(EndPoint endPoint)
    {
        this.Bind(endPoint);
        
        this.acceptTask = this.LoopAcceptAsync();

        return Task.CompletedTask;
    }
    
    private void Bind(EndPoint point)
    {
        if (this.listenSocket != null)
        {
            throw new InvalidOperationException("Transport is already bound.");
        }

        this.localAddress = point;
        Socket socket;
        switch (this.localAddress)
        {
            case IPEndPoint ip:
                socket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                if (Equals(ip.Address, IPAddress.IPv6Any))
                {
                    socket.DualMode = true;
                }
                BindSocket();
                break;
            default:
                socket = new Socket(this.localAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                BindSocket();
                break;
        }

        void BindSocket()
        {
            try
            {
                socket.Bind(this.localAddress);
            }
            catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse)
            {
                throw;
            }
        }

        this.localAddress = socket.LocalEndPoint;
        socket.Listen(this.featureOptions.Backlog);
        this.listenSocket = socket;
    }
    
    private async Task LoopAcceptAsync()
    {
        while (true)
        {
            var socket = await this.AcceptAsync();
            if (socket == null)
            {
                return;
            }

            await this.StartSocketTransport(socket);
        }
    }

    private async ValueTask StartSocketTransport(Socket socket)
    {
        TcpSocketFeatureSetting.SettingFeature(socket, this.featureOptions);
        var connection = this.CreateConnection();
        var setting = this.channelSettingsAllotter.Next();
        var transmitter = this.transportFactory(socket, setting, connection);
        await transmitter.StartAsync();
    }


    private async ValueTask<Socket> AcceptAsync()
    {
        while (true)
        {
            try
            {
                return await this.listenSocket.AcceptAsync();
            }
            catch (ObjectDisposedException)
            {
                return null;
            }
            catch (SocketException e) when (e.SocketErrorCode == SocketError.OperationAborted)
            {
                return null;
            }
            catch (SocketException)
            {
                // The connection got reset while it was in the backlog, so we try again.
            }
        }
    }

    public override async Task StopAsync()
    {
        if (this.listenSocket != null)
        {
            this.listenSocket.Dispose();
        }

        await acceptTask;
    }
}